6.Web层

6.1 控制器(Controllers)

一个控制器(Controllers)处理请求并创建或准备响应 ,是请求范围。 换句话说,会为每个 request 创建一个新的实体。 一个控制器(Controller)可以生成响应或委托给视图。 创建一个控制器(Controller)只需要创建一个以 Controller 结尾的类。并放置于 grails-app/controllers 目录下。

默认的 URL Mapping 设置确保控制器(Controllers)名字的第一个部分被映射到URI上 ,每个在控制器(Controllers)中定义的操作(Action)被映射到控制器(Controller)名字URI中的URI。

6.1.1 理解控制器(Controller)与操作(Action)

创建控制器(Controller)

可以通过 create-controller 创建控制器(Controllers)。例如,你可以在Grails项目的根目录尝试运行下面命令:

grails create-controller book

这条命令将会在grails-app/controllers/BookController.groovy路径下创建一个控制器(Controller):

class BookController { … }

BookController 默认被映射到 /book URI(相对于你应用程序根目录).

create-controller 命令只不过是方便的工具,你同样可以使用你喜欢的文本编辑器或IDE更容易的创建控制器(Controller)

创建操作(Action)

一个控制器(Controllers) 可以拥有多个属性,每个属性都可以被分配一个代码块。所有这样的属性都被映射到URI:

class BookController {
    def list = {

// do controller logic // create model

return model } }

默认情况下,由于上面示例属性名被命名为list所以被映射到/book/list URI。

默认Action

一个控制器(Controller)具有默认 URI概念,即被映射到 控制器(Controller)的根URI。默认情况下,默认的URI是/book。 默认的URI通过下面的规则来规定:

def defaultAction = "list"

6.1.2 控制器(Controller) 与作用域

可用的作用域

作用域本质上就是hash对象,它允许你存储变量。 下面的作用域在 控制器(Controller)中可以使用:

存取作用域

作用域可以通过上面的变量名与Groovy数组索引操作符结合来进行存取。甚至是Servlet API提供的类,像 HttpServletRequest:

class BookController {
    def find = {
        def findBy = params["findBy"]
        def appContext = request["foo"]
        def loggedUser = session["logged_user"]

} }

你设置可以使用.操作符来存取作用域中的变量,这是语法更加清楚:

class BookController {
    def find = {
        def findBy = params.findBy
        def appContext = request.foo
        def loggedUser = session.logged_user

} }

这是Grails统一存取不同作用域的一种方式。

使用Flash作用域

Grails 支持 flash作用域的概念,它只用于临时存储用于这个请求到下个请求的属性,然后,这个属性就会被清除 对于重定向前直接设置消息是非常有用的,例如:

def delete = {
    def b = Book.get( params.id )
    if(!b) {
        flash.message = "User not found for id ${params.id}"
        redirect(action:list)
    }
    … // remaining code
}

6.1.3 Models(模型)与Views(视图)

Returning the Model

一个model本质上就是一个map,在视图渲染时使用。map中的keys转化为变量名,用于视图的获取。 第一种方式是明确的return一个model:

def show = {
 	[ book : Book.get( params.id ) ]
}

如果没有明确的 model被return,控制器(Controller)的属性将会被视为 model。 所以允许你这样编写代码:

class BookController {
    List books
    List authors
    def list = {
           books = Book.list()
           authors = Author.list()
    }
}

这可能由于实际上控制器(Controller)是 prototype(原型)范围。换句话说,每个请求都会创建一个新的控制器(Controller)。 否则,像上面的代码,就不会是线程安全的。

上面示例中,booksauthors属性在视图中都是可用的。

一个更高级的方式就是 return一个 Spring ModelAndView 类的实体:

import org.springframework.web.servlet.ModelAndView

def index = { def favoriteBooks = … // get some books just for the index page, perhaps your favorites

// forward to the list view to show them return new ModelAndView("/book/list", [ bookList : favoriteBooks ]) }

选择View

在之前的2个示例中,都没有指定哪个 view 用于渲染。因此,Grails怎么知道哪个 view被选取?答案在于规约。对于action:

class BookController {
	def show = {
	 	[ book : Book.get( params.id ) ]
	}	
}

Grails 会自动查找位于 grails-app/views/book/show.gspview (事实上, Grails 会首先查找JSP,因为,Grails同样可以与 JSP一起使用).

假如,你想渲染其他view, render 方法在这里就能帮助你:

def show = {
  	def map = [ book : Book.get( params.id ) ]
    render(view:"display", model:map)
}

这种情况下,Grails将会尝试渲染位于 grails-app/views/book/display.gsp的view。注意,Grails自动描述位于book文件夹中的 grails-app/views路径位置的视图。很方便,但是,如果你拥有某些共享的视图需要存取,作为替代使用:

def show = {
  	def map = [ book : Book.get( params.id ) ]
    render(view:"/shared/display", model:map)
}

在这种情况下,Grails将尝试渲染grails-app/views/shared/display.gsp位置上的视图。

渲染响应

有时它很容易的渲染来自创建控制器小块文本或者代码的响应(通常使用Ajax应用程序)。因为,使用高度灵活的 render方法:

render "Hello World!"

上面的代码,在响应中写入 "Hello World!"文本, 其他的示例包括:

// write some markup
render {
   for(b in books) {
      div(id:b.id, b.title)
   }
}
// render a specific view
render(view:'show')
// render a template for each item in a collection
render(template:'book_template', collection:Book.list())
// render some text with encoding and content type
render(text:"<xml>some xml</xml>",contentType:"text/xml",encoding:"UTF-8")

如果,你打算使用Groovy的MarkupBuilder来产生html,可以使用render来避免html元素与Grails标签之间的命名冲突。例如:

def login = {
        StringWriter w = new StringWriter()
        def builder = new groovy.xml.MarkupBuilder(w)
        builder.html{
            head{
                title 'Log in'
            }
            body{
                h1 'Hello'
                form{

} } }

def html = w.toString() render html }

实际上调用 form标签 (将返回一些文本,而忽略MarkupBuilder). 为了正确的输出 <form>元素,使用下面这些:

def login = {
        // …
        body{
            h1 'Hello'
            builder.form{

} } // … }

6.1.4 重定向与链接

Redirects

使用redirect方法,Actions(操作)可在所有的控制器(Controller)中重定向:

class OverviewController {
                     def login = {}

def find = { if(!session.user) redirect(action:login) … } }

redirect 方法内部使用HttpServletResonse对象的sendRedirect方法。

redirect 方法可以选择如下用法之一:

// 调用同一个类的login action
                 redirect(action:login)

// 重定向到home 控制器(Controller)的index action
                 redirect(controller:'home',action:'index')

// 明确的重定向到URI
                 redirect(uri:"/login.html")

// 重定向到一个URL
 redirect(url:"http://grails.org")

使用方法的params 参数,参数可以选择性的从一个 action传递到下一个:

redirect(action:myaction, params:[myparam:"myvalue"])

通过 params动态属性,这些方法变得可用,同样也接受request参数。 如果指定一个名字与request参数的名字相同的参数,则 request参数被隐藏,控制器(Controller)参数被使用。

因为 params对象也是一个 map,可以使用它把当前的request参数,从一个 action传递到下一个:

redirect(action:"next", params:params)

最后,你也可以在一个目标URI上包含一个片段(fragment):

redirect(controller: "test", action: "show", fragment: "profile")

将(依靠 URL mappings) 导航到/myapp/test/show#profile"。

h4. 链接

Actions同样可以被链接。链接允许model在一个操作(Action)到下一个操作(Action)中保留。例如下面调用first action :

class ExampleChainController {
                     def first = {
                         chain(action:second,model:[one:1])
                     }
                     def second  = {
                         chain(action:third,model:[two:2])
                     }
                     def third = {
                          [three:3])
                     }
                 }

model的结果:

[one:1, two:2, three:3]

通过chainModel map,这个 model在chain中会被随后的 控制器(controller)操作(actions)存取. 这个动态属性只存在于随后调用chain方法的操作(actions)中:

class ChainController {

def nextInChain = { def model = chainModel.myModel … } }

Like the redirect method you can also pass parameters to the chain method:

chain(action:"action1", model:[one:1], params:[myparam:"param1"])

6.1.5 Controller(控制器) 拦截器

通常,它用于拦截基于每个request(请求),session(会话)或应用程序状态的数据处理,这可以通过 action(操作)拦截器来实现。 目前有两种拦截器类型: before 和 after.

假如你的拦截器可能被用于更多的controller(控制器), 几乎肯定会写一个更好的 Filter(过滤器). Filters(过滤器) 可以应用于多个controllers(控制器)或 URIs, 无需改变任何controller(控制器)逻辑.

Before 拦截器

beforeInterceptor在action (操作)被执行前进行数据处理拦截 . 假如它返回 false,那么 ,被拦截的action (操作)将不会被执行. 拦截器可以像下面这样被定义为拦截一个controller(控制器)中所有的action (操作):

def beforeInterceptor = {
       println "Tracing action ${actionUri}"
}

上面是在controller(控制器)定义主体内被声明. 它会在所有 action(操作)之前被执行,并且不会干扰数据处理. 一个普通的使用情形是为了验证:

def beforeInterceptor = [action:this.&auth,except:'login']
// defined as a regular method so its private
def auth() {
     if(!session.user) {
            redirect(action:'login')
            return false
     }
}
def login = {
     // display login page
}

上面的代码定义了一个名为auth的方法. 使用一个方法,是为了让它不会作为一个 action(操作)而暴露于外界(即. 它是private). 随后,beforeInterceptor 定义用于'except' login actions(操作)之外的所有 actions(操作)的拦截,并告知执行'auth' 方法. 'auth' 方法是使用Groovy的方法指针语法来引用 ,在方法内部,它自己会检测是否一个用户在session(会话)内,否则,重定向到 login action(操作) 并返回 false, 命令被拦截的actions(操作)不被执行 .

After 拦截器

为了定义一个在actions(操作)之后执行的拦截,可以使用afterInterceptor 属性:

def afterInterceptor = { model ->
       println "Tracing action ${actionUri}"
}

after 拦截器把结果 model作为参数,所以,可以执行model或response的post操作.

after 拦截器 也可以在渲染之前修改Spring MVC ModelAndView对象. 在这种情况下, 上面的示例变成:

def afterInterceptor = { model, modelAndView ->
       println "Current view is ${modelAndView.viewName}"
       if(model.someVar) modelAndView.viewName = "/mycontroller/someotherview"
       println "View is now ${modelAndView.viewName}"
}

通过当前action(操作),允许基于被返回的model改变视图. 注意,如果action(操作)被拦截调用redirect 或render, modelAndView 可能为null.

拦截条件

Rails 用户非常熟悉验证示例 ,以及如何在'except'条件的使用下执行拦截 (拦截器在Rails中被称为'过滤器', 这个术语与Java领域中的servlet 过滤器术语有冲突):

def beforeInterceptor = [action:this.&auth,except:'login']

除了被指定的actions(操作),它执行所有actions(操作)的拦截. 一组actions(操作)列表同样可以像下面这样被定义:

def beforeInterceptor = [action:this.&auth,except:['login','register']]

其他被支持的条件是'only', 它只对被指定的actions(操作)执行拦截:

def beforeInterceptor = [action:this.&auth,only:['secure']]

6.1.6 数据绑定

数据绑定是"绑定"进入的请求参数到一个对象的属性或者一个完整对象图的行为. 数据绑定将处理所有来自请求参数必要的类型装换,典型的传送通过表单提交 , 始终是字符串,尽管Groovy或Java对象的属性可能不一定是.

Grails使用 Spring's底层的数据绑定能力来完成数据绑定.

绑定Request数据到Model上

这里有2种方式来绑定请求参数到domain类的属性上. 第一种涉及使用domain类的隐式构造函数:

def save = {
  def b = new Book(params)
  b.save()
}

这里的数据绑定发生在代码new Book(params)内.通过传递 params 对象给domain类的构造函数, Grails 自动识别来自请求参数的绑定 . 因此,假如你有一个这样进入的请求 :

/book/save?title=The%20Stand&author=Stephen%20King

titleauthor 请求参数将会自动 被设置到domain类上. 假如,你需要在一个已存在的实体上执行数据绑定,那么你可以使用 properties 属性:

def save = {
  def b = Book.get(params.id)
  b.properties = params
  b.save()
}

这个和使用隐式构造函数是完全一样的效果.

数据绑定和单向关联

如果你有one-to-onemany-to-one 关联,你同样可以使用Grails的数据绑定能力更新这些关系. 例如,如果你有这样的请求参数:

/book/save?author.id=20

Grails 将自动检测请求参数上的 .id 后缀,并查找给定id的 Author实体 ,随后像这样进行数据绑定:

def b = new Book(params)

属于绑定与Many-ended关联

假如你有一个 one-to-many 或 many-to-many关联,依赖关联类型,有不同的方法用于数据绑定.

假如你有一个以Set基本的关联 (默认用于hasMany) ,那么简单的方式加入一个关联是简单的传送一组标识符列表. 考虑下面 <g:select> 示例的用法:

<g:select name="books"
          from="${Book.list()}"
          size="5" multiple="yes" optionKey="id"
          value="${author?.books}" />

它生成一个选择框 ,允许你选择多个值. 在这种情况下,如果你提交表单,Grails将自动利用来自选择框的标识符加入 books关联.

不过, 假如,你有一个更新关联对象的属性的方案,这个方法将不会工作. 作为替代,你需要使用下标操作符:

<g:textField name="books[0].title" value="the Stand" />
<g:textField name="books[1].title" value="the Shining" />

不过, 如果,你想要更新在相同顺序中的渲染标记,对于基于Set的关联是危险的 . 这是因为Set 没有顺序的概念, 所以,你引用的books0books1 不能确保关联的顺序在服务器端的正确性,除非你自己应用明确排序 .

如果你使用基于List的关联就不会存在这个问题 , 因为List 拥有确定的顺序并使供索引来引用. 这同样适用于基于 Map的关联.

还要注意 ,假如你绑定的关联长度为,你引用的元素超出了关联的长度:

<g:textField name="books[0].title" value="the Stand" />
<g:textField name="books[1].title" value="the Shining" />
<g:textField name="books[2].title" value="Red Madder" />

随后, Grails 在确定的位置自动为你创建一个实体. 如果你"跳过"中间的某些元素 :

<g:textField name="books[0].title" value="the Stand" />
<g:textField name="books[1].title" value="the Shining" />
<g:textField name="books[5].title" value="Red Madder" />

随后,Grails会自动在中间创建实体 . 例如,如果关联的长度为2,在上面的情况下,Grails会创建4 个额外的实体.

数据绑定多个domain类

它可能通过来自 params对象来绑定多个domain对象.

例如,你有一个进入的请求:

/book/save?book.title=The%20Stand&author.name=Stephen%20King

需要注意的是,上面请求不同之处在于拥有 author.前缀或 book前缀. 这是用于分离哪个参数属于哪个类型. Grails的params对象就像 多维 hash ,你可以索引来分离唯一的参数子集来绑定.

def b = new Book(params['book'])

注意,我们如何使用book.title的第一圆点前面的前缀参数来隔离唯一的参数绑定. 我们同样可以这样来使用Authordomain类 :

def a = new Author(params['author'])

数据绑定与类型转换错误

有时,当执行数据绑定时,它可能不会将一种指定的String转换为指定的目标类型. 你会得到类型转换错误. Grails 会保留类型转换错误在Grails domain 类的 errors 属性中 . 例如这里:

class Book {
    …
    URL publisherURL
}

这里,我们有一个Bookdomain 类 ,它使用Java的java.net.URL来表示 URLs.现在,我们有一个像这样的请求参数:

/book/save?publisherURL=a-bad-url

在这种情况下,它不可能将 字符串a-bad-url 绑定到 publisherURL 属性上,一个类型匹配错误会发生. 你可以像这样来检查它们:

def b = new Book(params)

if(b.hasErrors()) { println "The value ${b.errors.getFieldError('publisherURL').rejectedValue} is not a valid URL!" }

虽然,我们没有覆盖错误代码 (更多信息查看 Validation), 你需要的类型转换错误的错误消息在grails-app/i18n/messages.properties 内. 你可以使用像下面这样的普通错误消息来处理 :

typeMismatch.java.net.URL=The field {0} is not a valid URL

或更具体点:

typeMismatch.Book.publisherURL=The publisher URL you specified is not a valid URL

数据绑定与安全关系

当批量更新来自请求参数的属性,你必须小心,避免客户端绑定恶意数据到 domain 类上, 并持久化到数据库.你可以使用下标操作符限制捆绑在某个给定domain类的属性:

def p = Person.get(1)

p.properties['firstName','lastName'] = params

在这种情况下,只有firstNamelastName 属性将被捆绑.

另一种实现这个的方式是使用 domain类作为数据绑定目标,你可以使用Command Objects. 另外还有一个更加灵活bindData 方法.

The bindData 方法具有同样的数据绑定能力,但,是对于任意的对象:

def p = new Person()
bindData(p, params)

当然,bindData 方法同样允许你排除某些你不想更新的参数:

def p = new Person()
bindData(p, params, [exclude:'dateOfBirth'])

或只包含某些属性:

def p = new Person()
bindData(p, params, [include:['firstName','lastName]])

6.1.7 XML与JSON响应

使用render方法输出XML

Grails支持一些不同的方法来产生XML和JSON响应. 第一个是通过 render 方法.

render方法可以传递一个代码块来实现XML中的标记生成器:

def list = {
	def results = Book.list()
	render(contentType:"text/xml") {
		books {
			for(b in results) {
				book(title:b.title)
			}
		}	
	}
}

这段代码的结果会像这样:

<books>
	  <book title="The Stand" />
	  <book title="The Shining" />	
</books>

注意,你必须小心的是避免使用标记生成器带来的命名冲突. 例如,这段代码会产生一个错误:

def list = {
	def books = Book.list()  // naming conflict here
	render(contentType:"text/xml") {
		books {
			for(b in results) {
				book(title:b.title)
			}
		}	
	}
}

问题在于,这里的局部变量 books, Groovy会把它当做一个方法来调用.

使用render方法输出JSON

render 同样被用于输出JSON:

def list = {
	def results = Book.list()
	render(contentType:"text/json") {
		books {
			for(b in results) {
				book(title:b.title)
			}
		}	
	}
}

在这种情况下,结果大致相同:

[
	{title:"The Stand"}, 
	{title:"The Shining"}
]

同样的命名冲突危险适用于JSON生成器.

自动XML列集(Marshalling)

(译者注:在此附上对于列集(Marshalling)解释:对函数参数进行打包处理得过程,因为指针等数据,必须通过一定得转换,才能被另一组件所理解。可以说列集(Marshalling)是一种数据格式的转换方法。)

Grails同样支持自动列集(Marshalling) domain类 为XML,通过特定的转换器.

首先,导入grails.converters 类包到你的controller(控制器):

import grails.converters.*

现在,你可以使用下列高度易读的语法来自动转换domain类为XML:

render Book.list() as XML

输出结果看上去像下面这样:

<?xml version="1.0" encoding="ISO-8859-1"?>
<list>
  <book id="1">
    <author>Stephen King</author>
    <title>The Stand</title>
  </book>
  <book id="2">
    <author>Stephen King</author>
    <title>The Shining</title>
  </book>
</list>

一个使用转换器的替代方法是使用Grails的codecs 特性. codecs特性提供了 encodeAsXMLencodeAsJSON方法:

def xml = Book.list().encodeAsXML()
render xml

更多的XML 列集(Marshalling)信息见REST

自动JSON列集(Marshalling)

Grails同样支持自动列集(Marshalling)为JSON通过同样的机制. 简单替代XMLJSON:

render Book.list() as JSON

输出结果看上去像下面这样:

[
	{"id":1,
	 "class":"Book",
	 "author":"Stephen King",
	 "title":"The Stand"},
	{"id":2,
	 "class":"Book",
	 "author":"Stephen King",
	 "releaseDate":new Date(1194127343161),
	 "title":"The Shining"}
 ]

作为替代,你可以使用encodeAsJSON达到相同的效果.

6.1.8 文件上传

文件上传程序

Grails通过Spring的 MultipartHttpServletRequest 接口来支持文件上传. 上传文件的第一步就是像下面这样创建一个multipart form:

Upload Form: <br />
	<g:form action="upload" method="post" enctype="multipart/form-data">
		<input type="file" name="myFile" />
		<input type="submit" />
	</g:form>

这里有一些方法来处理文件上传. 第一种方法是直接与Spring的MultipartFile 实体:

def upload = {
    def f = request.getFile('myFile')
    if(!f.empty) {
      f.transferTo( new File('/some/local/dir/myfile.txt') )
      response.sendError(200,'Done');
    }    
    else {
       flash.message = 'file cannot be empty'
       render(view:'uploadForm')
    }
}

这显然很方便,通过MultipartFile 接口可以直接获得一个InputStream,用来转移到其他目的地和操纵文件等等.

通过数据绑定上传文件

文件上传同样可以通过数据绑定来完成。例如,假定你有一个像下面这样Image domain类:

class Image {
   byte[] myFile
}

现在,假如你创建一个image并像下面这个示例一样传入 params对象,Grails将自动把文件的内容当作一个byte绑定到myFile属性:

def img = new Image(params)

它同样可以设置文件的内容为一个string,通过改变image的myFile属性类型为一个String类型:

class Image {
   String myFile
}

6.1.9 命令对象

Grails控制器(controllers)支持命令对象概念.一个命令对象类似于Struts中的一个formbean,它们在当你想要写入属性子集来更新一个domain类情形时是非常有用的 . 或在没有domain类需要的相互作用,但必须使用 data bindingvalidation 特性 .

声明命令对象

命令对象通常作为一个控制器直接声明在控制器(controller)类定义下的同一个源文件中. 例如:

class UserController {
	…
}
class LoginCommand {
   String username
   String password
   static constraints = {
           username(blank:false, minSize:6)
           password(blank:false, minSize:6)
   }
}

上面的示例证明你可以提供 约束给命令对象,就象你在domain 类中的用法一样.

使用命令对象

为了使用命令对象,控制器可以随意指定任何数目的命令对象参数。必须提供参数的类型以至于Grails能知道什么样的对象被创建,写入和验证.

在控制器(controller)的操作被执行之前,Grails将自动创建一个命令对象类的实体,用相应名字的请求参数写入到命令对象属性, 并且命令对象将被验证,例如:

class LoginController {
  def login = { LoginCommand cmd ->
         if(cmd.hasErrors()) {
                redirect(action:'loginForm')
         }
         else {
            // do something else
        }
  }
}

命令对象与依赖注入

命令对象可以参与依赖注入。这有利于一些定制的验证逻辑与Grails的services的结合。 :

class LoginCommand {
    def loginService

String username String password

static constraints = { username(validator: { val, obj -> obj.loginService.canLogin(obj.username, obj.password) }) } }

上面示例,命令对象与一个来自Spring的 ApplicationContext注入名字bean结合.

6.1.10 处理重复的表单提交

Grails 已经内置支持处理重复表单提交, 通过使用"同步令牌模式". 首先,你得在 form 标签上定义一个令牌:

<g:form useToken="true" ...>

随后,在你的控制器(controller)代码中使用 withForm 方法来处理有效和无效的请求:

withForm {
   // good request
}.invalidToken {
   // bad request
}

如果你只提供了 withForm 方法而没有链接 invalidToken 方法,那么,默认情况下,Grails 将会无效的令牌存储在flash.invalidToken变量中 并导航请求回到原始页面. 这可以在页面中检测到:

<g:if test="${flash.invalidToken}">
  Don't click the button twice!
</g:if>

withForm 标签利用了session ,因此,如果在群集中使用,要求会话密切关联.

6.2 Groovy Server Pages

Groovy Servers Pages (或者简写为 GSP)Grails的视图技术。它被设计成像ASP和JSP这样被使用者熟悉的技术,但更加灵活和直观.

GSP存在于Grails的grails-app/views目录中,他们通常会自动渲染(通过规约),或者像这样通过render方法:

render(view:"index")

GSP使典型的混合标记和GSP标签,辅助页面渲染.

虽然,它可能会在你的GSP页面中内置Groovy逻辑,Although it is possible to have Groovy logic embedded in your GSP and doing this will be covered in this document the practice is strongly discouraged. Mixing mark-up and code is a bad thing and most GSP pages contain no code and needn't do so.

一个GPS通常拥有一个"model",它是变量集被用于视图渲染。通过一个控制器model被传递到GSP视图。例如,考虑下列控制器的操作:

def show = {
	[book: Book.get(params.id)]
}

这个操作将查找一个book实体,并创建一个包含关键字为Book的model,这个关键字可在随后的GSP视图中应用:

<%=book.title%>

6.2.1 GSP基础

在下一节,我们将通过GSP基础知识让你知道它能做什么。首先,我们将涵盖基础语法,对于JSP和ASP用户是非常熟悉的.

GSP支持使用<% %>来嵌入Groovy代码(这是不推荐的):

<html>
   <body>
     <% out << "Hello GSP!" %>
   </body>
</html>

同样,你可以使用<%= %>语法来输出值:

<html>
   <body>
     <%="Hello GSP!" %>
   </body>
</html>

GSP同样支持服务器端JSP样式注释,像下列示例显示的这样:

<html>
   <body>
	 <%-- This is my comment --%>
     <%="Hello GSP!" %>
   </body>
</html>

6.2.1.1 变量与作用域

<% %> 中你当然可以声明变量:

<% now = new Date() %>

然后,在页面中的之后部分可以重复使用 :

<%=now%>

然而, 在GSP中存在着一些预先定义的变量,包括:

6.2.1.2 逻辑和迭代

使用 <% %> 语法,你当然可以使用这样的语法进行嵌套循环等等操作:

<html>
   <body>
      <% [1,2,3,4].each { num -> %>
         <p><%="Hello ${num}!" %></p>
      <%}%>
   </body>
</html>

同样可以分支逻辑:

<html>
   <body>
      <% if(params.hello == 'true' )%>	
      <%="Hello!"%>
      <% else %>
      <%="Goodbye!"%>
   </body>
</html>

6.2.1.3 页面指令

GSP同样支持少许的JSP样式页面指令.

import指令允许在页面中导入类。然而,它却很少被使用,因为Groovy缺省导入和GSP 标签:

<%@ page import="java.awt.*" %>

GSP同样支持contentType@ 指令:

<%@ page contentType="text/json" %>

contentType@指令允许GSP使用其他的格式来渲染.

6.2.1.4 表达式

尽管GSP也支持 <%= %> 语法,而且很早就介绍过,但在实际当中却很少应用,因为此用法主要是为ASP和 、JSP开发者所保留的。 而GSP的表达式跟JSP EL表达式很相似的,跟Groovy GString的 ${expr} 用法也很像:

<html>
  <body>
    Hello ${params.name}
  </body>
</html>

尽管如此,跟JSP EL不同的是, 你可以在${..}括号中使用Groovy表达式.${..}中的变量缺省情况下是转义, 因此变量的任何HTML字符串内容被直接输出到页面,要减少这种Cross-site-scripting (XSS)攻击的风险, 你可以设置grails-app/conf/Config.groovy中的grails.views.default.codec为HTML转化方式:

grails.views.default.codec='html'

其他可选的值是'none' (缺省值)和'base64'.

6.2.2 GSP标签

现在,JSP遗传下来的缺点已经被取消,下面的章节将涵盖GSP的内置标签,它是定义GSP页面最有利的方法.

标签库 部分涵盖怎么添加你自己的定制标签库.

所有GSP内置标签以前缀g:开始。 不像JSP,你不需要指定任何标签库的导入.假如,一个标签以g:开始,它被自动认为是一个GSP标签.一个GPS标签的示例看起来像这样:

<g:example />

GSP标签同样可以拥有主体,像这样:

<g:example>
   Hello world
</g:example>

表达式被传递给GSP标签属性,假如没有使用表达式,将被认为是一个String值:

<g:example attr="${new Date()}">
   Hello world
</g:example>

Maps同样能被传递给GSP标签属性,通常使用一个命名参数样式语法:

<g:example attr="${new Date()}" attr2="[one:1, two:2, three:3]">
   Hello world
</g:example>

注意,对于String类型属性值,你必须使用单引号:

<g:example attr="${new Date()}" attr2="[one:'one', two:'two']">
   Hello world
</g:example>

在介绍完基本的语法之后,下面我们来讲解Grails中默认提供的标签.

6.2.2.1 变量与作用域

变量可以在GSP中使用 set 标签来定义:

<g:set var="now" value="${new Date()}" />

这里, 我们给GSP表达式结果赋予了一个名为now的变量 (简单的构建一个新的 java.util.Date 实体)。 你也可以在<g:set>主体中定义一个变量:

<g:set var="myHTML">
   Some re-usable code on: ${new Date()}
</g:set>

变量同样可以被放置于下列的范围内:

选择变量被放入的范围可以使用scope属性:

<g:set var="now" value="${new Date()}" scope="request" />

6.2.2.2 逻辑和迭代

GSP同样支持迭代逻辑标签,逻辑上通过使用 if , , elseelseif 来支持典型的分支情形。 :

<g:if test="${session.role == 'admin'}">
   <%-- show administrative functions --%>
</g:if>
<g:else>
   <%-- show basic functions --%>
</g:else>

GSP用each eachwhile标签来处理迭代:

<g:each in="${[1,2,3]}" var="num">
   <p>Number ${num}</p>
</g:each>

<g:set var="num" value="${1}" /> <g:while test="${num < 5 }"> <p>Number ${num++}</p> </g:while>

6.2.2.3 搜索和过滤

假如你拥有对象集合,你经常需要使用一些方法来排序和过滤他们。 GSP支持 findAllgrep 来做这些工作:

Stephen King's Books:
<g:findAll in="${books}" expr="it.author == 'Stephen King'">
     <p>Title: ${it.title}</p>
</g:findAll>

expr属性包含了一个Groovy表达式,它可以被当作一个过滤器来使用。 谈到过滤器,grep标签通过类来完成与过滤器类似的工作:

<g:grep in="${books}" filter="NonFictionBooks.class">
     <p>Title: ${it.title}</p>
</g:grep>

或者使用一个正则表达式:

<g:grep in="${books.title}" filter="~/.*?Groovy.*?/">
     <p>Title: ${it}</p>
</g:grep>

上面的示例同样有趣,因为它使用了GPath.Groovy的GPath等同与XPath语言。实际上books集合是books集合的实体。 不过,假设每个books拥有一个title,你可以使用表达式books.title来获取Book titles的list!

6.2.2.4 链接和资源

GSP还拥有特有的标签来帮助你管理连接到控制器和操作. link 标签允许你指定控制器和操作配对的名字,并基于 URL Mappings 映射来自动完成连接。即使你去改变!一些 link 的示例如下:

<g:link action="show" id="1">Book 1</g:link>
<g:link action="show" id="${currentBook.id}">${currentBook.name}</g:link>
<g:link controller="book">Book Home</g:link>
<g:link controller="book" action="list">Book List</g:link>
<g:link url="[action:'list',controller:'book']">Book List</g:link>
<g:link action="list" params="[sort:'title',order:'asc',author:currentBook.author]">
     Book List
</g:link>

6.2.2.5 表单和字段

表单基础

GSP支持许多不同标签来帮助处理HTML表单和字段,最基础的是form标签, form标签是一个控制器/操作所理解的正规的HTML表单标签版本。 url属性允许你指定映射到哪个控制器和操作:

<g:form name="myForm" url="[controller:'book',action:'list']">...</g:form>

我们创建个名为myForm的表单,它被提交到 BookControllerlist操作。除此之外,适用于所有不同的HTML属性.

表单字段

同构造简单的表单一样,GSP支持如下不同字段类型的定制:

上面的每一个都允许GSP表达式作为值:

<g:textField name="myField" value="${myValue}" />

GSP同样包含上面标签的扩张助手版本, 比如radioGroup (创建一组radio标签), localeSelect, currencySelecttimeZoneSelect(选择各自的地区区域, 货币 和时间区域). .

多样的提交按钮

处理多样的提交按钮这样由来已久的问题,同样可以通过Grails的actionSubmit 标签优雅的处理。它就像一个正规提交,但是,允许你指定一个可选的操作来提交:

<g:actionSubmit value="Some update label" action="update" />

6.2.2.6 标签作为方法调用

GSP标签和其他标签技术一个主要不同在于,来自 controllers(控制器) , 标签库 或者GSP 视图中的GPS标签可以被当作任意的正规标签或者当作方法被调用.

来自GSPs中的标签当作方法调用

当作为方法被调用时,标签的返回值被当作String实体直接被写入响应中。 因此,示例中的createLinkTo能等同的看做方法调用:

Static Resource: ${createLinkTo(dir:"images", file:"logo.jpg")}

当你必须在一个属性内使用一个标签时是特别有用的:

<img src="${createLinkTo(dir:'images', file:'logo.jpg')}" />

I在视图技术中,标签内嵌套标签的特性是不被支持的,这样变得十分混乱,往往使得像Dreamweaver这样WYSWIG的工具产生不利的效果以至于在渲染标签时:

<img src="<g:createLinkTo dir="images" file="logo.jpg" />" />

来自控制器(Controllers)和标签库的标签作为方法调用

你同样可以调用来自控制器和标签库的标签。标签可以不需要内部默认的g:namespace前缀来调用,并返回String结果:

def imageLocation = createLinkTo(dir:"images", file:"logo.jpg")

然而,你同样可以用命名空间前缀来避免命名冲突:

def imageLocation = g.createLinkTo(dir:"images", file:"logo.jpg")

假如你有一个自定义命名空间,,你可以使用它的前缀来替换(例如,使用 FCK Editor plugin:

def editor = fck.editor()

6.2.3 视图(View)与模板(Templates)

除了views之外, Grails还有模板的概念. 模板有利于分隔出你的视图在可维护的块中,并与 Layouts 结合提供一个高度可重用机制来构建视图.

模板基础

Grails使用在一个视图名字前放置一个下划线来标识为一个模板的规约。 例如,你可能有个位于grails-app/views/book/_bookTemplate.gsp的模板处理渲染Books:

<div class="book" id="${book?.id}">
   <div>Title: ${book?.title}</div>
   <div>Author: ${book?.author?.name}</div>
</div>

为了渲染来自grails-app/views/book视图中的一个模板,你可以使用render标签:

<g:render template="bookTemplate" model="[book:myBook]" />

注意,我们是怎么样使用render标签的model属性来使用传入的一个model。 假如,你有多个Book实体,你同样可以使用render标签为每个Book渲染模板 :

<g:render template="bookTemplate" var="book" collection="${bookList}" />

共享模板

在早先的示例中,我们有一个特定于BookController模板,它的视图位于grails-app/views/book.然而,你可能想横跨你的应用来共享模板。

在这种情况下,你可以把他们放置于grails-app/views视图根目录或者位于这个位置的任何子目录,然后在模板属性在模板名字之前使用一个 /来指明相对模板路径 .例如,假如你有个名为grails-app/views/shared/_mySharedTemplate.gsp模板, 你可以像下面这样引用它:

<g:render template="/shared/mySharedTemplate" />

你也可以使用这个技术从任何视图或控制器(Controllers)来引用任何目录下的模板:

<g:render template="/book/bookTemplate" model="[book:myBook]" />

模板命名空间

因为模板使用如此频繁,它有一个模板命名空间, 名为tmpl, 他使模板的使用变得容易. 考虑下面例子的使用模式:

<g:render template="bookTemplate" model="[book:myBook]" />

这个想下面这样通过tmpl命名空间表示 :

<tmpl:bookTemplate book="${myBook}" />

在控制器(Controllers)和标签库中的模板

你同样可以使用控制器 render方法渲染模板控制器中,它对Ajax引用很有用:

def show = {
    def b = Book.get(params.id)
	render(template:"bookTemplate", model:[book:b])
}

在控制器(controller)中的render 方法最普通的行为是直接写入响应。 假如,你需要获得模板作为一个String的结果作为替代,你可以使用render标签:

def show = {
    def b = Book.get(params.id)
	String content = g.render(template:"bookTemplate", model:[book:b])
	render content
}

注意, g. 命名空间的用法,它告诉Grails我们想使用标签作为方法调用来代替render 方法.

6.2.4 使用Sitemesh布局

创建布局

Grails利用了Sitemesh,一个装饰引擎,来支持视图布局。 布局位于grails-app/views/layouts 目录中。一个典型的布局如下:

<html>
      <head>
          <title><g:layoutTitle default="An example decorator" /></title>
          <g:layoutHead />
      </head>
      <body onload="${pageProperty(name:'body.onload')}">
            <div class="menu"><!--my common menu goes here--></menu>
                 <div class="body">
                      <g:layoutBody />
                 </div>
            </div>
      </body>
</html>

关键的元素是layoutHead, layoutTitlelayoutBody标签的用法,这里是他们所做的:

上面的示例同样表明pageProperty tag 可被用于检查和返回目标页面的外观.

启用布局

这里有一些方法来启用一个布局.简单的在视图中添加meta标签:

<html>
    <head>
	    <title>An Example Page</title>
        <meta name="layout" content="main"></meta>
    </head>
    <body>This is my content!</body>
</html>

在这种情况下,一个名为grails-app/views/layouts/main.gsp将被用于布局这个页面。假如,我们使用来自早前部分的布局,输出看上去像下列这样:

<html>
      <head>
          <title>An Example Page</title>
      </head>
      <body onload="">
        <div class="menu"><!--my common menu goes here--></div>
                 <div class="body">
					This is my content!
                 </div>
      </body>
</html>

在控制器(Controller)中指定布局

另一种用于指定布局的方式是通过在控制器(controller)中为 "layout"属性指定布局的名字, 假如你有个这样的控制器(controller):

class BookController {
    static layout = 'customer'

def list = { … } }

你可以创建一个grails-app/views/layouts/customer.gsp布局,应用于所有 BookController中委派的视图 . "layout"属性值可能包含相对于grails-app/views/layouts/目录的路径结构 . 例如:

class BookController {
    static layout = 'custom/customer'

def list = { … } }

视图的显然可通过 grails-app/views/layouts/custom/customer.gsp 模板.

布局规约

第二种关联布局的方法是使用"布局规约",假如你有个这样的控制器:

class BookController {
    def list = {  … }
}

你可以创建一个名为grails-app/views/layouts/book.gsp的布局,根据规约,它将被应用于BookController的所有视图中。

换句话说,你可以创建一个名为grails-app/views/layouts/book/list.gsp的布局,它将只被应用于BookController中的list操作,

如果你同时使用了以上提到的两种布局的话,那当list操作被执行的时候,那么操作将根据优先级的顺序来使用布局.

内联布局

通过applyLayout标签Grails同样支持Sitemesh的内联布局概念。 applyLayout标签可以被用于应用一个布局到一个模板,URL或者内容的任意部分。 事实上,通过"decorating"你的模板允许你更进一步的积木化你的视图结构.

一些使用示例如下:

<g:applyLayout name="myLayout" template="bookTemplate" collection="${books}" />

<g:applyLayout name="myLayout" url="http://www.google.com" />

<g:applyLayout name="myLayout"> The content to apply a layout to </g:applyLayout>

Server-Side包含

applyLayout标签被以用于引用布局外内容 applying layouts to , 假如你想简单的在当前页面包含外部内容,你可以使用 include:

<g:include controller="book" action="list"></g:include>

你甚至可以结合 include 标签和 applyLayout 标签 来添加灵活性:

<g:applyLayout name="myLayout">
   <g:include controller="book" action="list"></g:include>
</g:applyLayout>

最后,你也可以在控制器(controller)或标签库把include标签作为方法调用 :

def content = include(controller:"book", action:"list")

最后的内容有 include标签的返回值提供 .

6.2.5 Sitemesh内容块

虽然,这对于装饰全部页面非常有用,有时,你需要装饰站点的部分独自的页面。为了实现这个可以使用内容块. 在开始时,你需要使用 <content>标签分隔装饰页面 :

<content tag="navbar">
… draw the navbar here…
</content>
<content tag="header">
… draw the header here…
</content>
<content tag="footer">
… draw the footer here…
</content>
<content tag="body">
… draw the body here…
</content>

随后,在布局内部,你可以引用这些组件并为每个引用单个布局:

<html>
	<body>
		<div id="header">
			<g:applyLayout name="headerLayout"><g:pageProperty name="page.header"></g:applyLayout>
		</div>
		<div id="nav">
			<g:applyLayout name="navLayout"><g:pageProperty name="page.navbar"></g:applyLayout>
		</div>
		<div id="body">
			<g:applyLayout name="bodyLayout"><g:pageProperty name="page.body"></g:applyLayout>
		</div>
		<div id="footer">
			<g:applyLayout name="footerLayout"><g:pageProperty name="page.footer"></g:applyLayout>			
		</div>
	</body>
</html>

6.3 标签库

Java Server Pages JSP) 一样,GSP支持定制tag库的概念.不同于JSP,Grails标签库机制是简单的,优雅的,在运行时完全可重载的.

创建一个标签库是相当简单的,创建一个以规约TagLib结尾的一个Groovy类,并把它放置于grails-app/taglib目录里:

class SimpleTagLib {

}

现在,为了创建一个标签,简单的创建属性并赋值一个带有两个参数的代码块:标签属性和主体内容:

class SimpleTagLib {
	def simple = { attrs, body ->

} }

attrs属性是一个简单的标签属性map,同时body是另一可调用的代码块,它返回主体内容:

class SimpleTagLib {
	def emoticon = { attrs, body ->
	   out << body() << attrs.happy == 'true' ? " :-)" : " :-("	
    }
}

正如以上所显示的,这里有个隐式的out变量,它引用了输出Writer,可以用来附加内容到响应中. 然后,你可以在你的GSP内简单的引用这个标签而不需要任何导入:

<g:emoticon happy="true">Hi John</g:emoticon>

6.3.1 变量与作用域

在标签库的作用域中包含了一些预先定义好的变量:

6.3.2 简单标签

作为演示,早先的示例只不过是写了个没有主体只有输出内容的简单标签。另一个示例是一个 dateFormat 样式标签:

def dateFormat = { attrs, body ->
	out << new java.text.SimpleDateFormat(attrs.format).format(attrs.date)
}

上面使用了Java的SimpleDateFormat类来格式化一个date,然后把它写入响应。随后,这个标签能像下列这样在GSP中使用:

<g:dateFormat format="dd-MM-yyyy" date="${new Date()}" />

有时。你需要用简单的标签把HTML标签(mark-up)写入到响应中。一个方法是直接嵌套内容:

def formatBook = { attrs, body ->
    out << "<div id="${attrs.book.id}">"	
    out << "Title : ${attrs.book.title}"	
	out << "</div>"
}

虽然,这个方法可能很诱人,但不是非常的简洁。一个更好的方法将是复用render标签:

def formatBook = { attrs, body ->
    out << render(template:"bookTemplate", model:[book:attrs.book])	
}

然后,这个单独的GSP模板做了实际的渲染工作.

6.3.3 逻辑标签

一旦一组条件满足,你同样可以在标签的主体中创建仅仅用来输出的逻辑标签。一个这样的例子可能是一组安全标签:

def isAdmin = { attrs, body ->
     def user = attrs['user']
     if(user != null && checkUserPrivs(user)) {
           out << body()
     }
}

上面的标签检查用户是否为管理人员,如果他/她有正确设置的访问权限只输出主体内容:

<g:isAdmin user="${myUser}">
    // some restricted content
</g:isAdmin>

6.3.4 迭代标签

迭代标签同样普通,因为你可以多次调用主体:

def repeat = { attrs, body ->
    attrs.times?.toInteger().times { num ->
        out << body(num)
    }
}

在这个示例中,我们检查一个times属性,假如存在,把它转换为一个数字,然后使用Groovy的times方法:

<g:repeat times="3">
<p>Repeat this 3 times! Current repeat = ${it}</p>
</g:repeat>

注意,我们是怎么样在这个示例中使用隐式的it变量来引用当前的数字。这个过程是因为在迭代内部我们调用了传递进入当前值的主体:

out << body(num)

那个值然后被作为默认的it变量传递给标签,然而,假如你有嵌套标签便会导致冲突,因此,你将可能替换主体使用的变量名:

def repeat = { attrs, body ->
	def var = attrs.var ? attrs.var : "num"
    attrs.times?.toInteger().times { num ->
        out << body((var):num)
    }
}

这里,我们检查是否存在一个var属性,如果存在的话,将其作为body调用的参数:

out << body((var):num)

注意,变量名围绕的圆括号的使用.假如你省略,Groovy会认为你使用了一个String关键字,而不是引用这个变量它自己.

现在,我们可以改变这个标签的使用方法,如下:

<g:repeat times="3" var="j">
<p>Repeat this 3 times! Current repeat = ${j}</p>
</g:repeat>

注意,我们是怎么样使用var属性来定义j变量名,随后,我们可能在标签主体类引用这个变量.

6.3.5 标签命名空间

默认情况下,标签被添加到默认的Grails命名空间,并在GSP页面中和 g: 前缀一起使用。然而,你可以指定一个不同的命名空间,通过在你的 TagLib 类中添加一个静态属性:

class SimpleTagLib {
    static namespace = "my"

def example = { attrs -> … } }

这里,我们指定了一个命名空间my,因此,稍后在GPS页面中标签库中的标签引用会像这样:

<my:example name="..." />

前缀和静态的命名空间属性值一样.命名空间对于插件特别有用.

命名空间内的标签可以作为方法调用,使用命名空间作为前缀来执行方法调用:

out << my.example(name:"foo")

可用于GSP,控制器或者标签库.

6.3.6 使用JSP 标签库

除了GSP提供的简单标签库机制, 你也可以在GSP中使用JSP标签.通过taglib指令来简单声明你需要的JSP标签:

<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>

随后,你可以想任何其他标签一样来使用它:

<fmt:formatNumber value="${10}" pattern=".00"/>

额外的好处是,你可以把JSP标签当方法调用 :

${fmt.formatNumber(value:10, pattern:".00")}

6.4 URL映射

到目前为止,贯穿整个文档用于URLs的规约默认为 /controller/action/id . 然而,这个规约不是硬性的写入Grails中,实际上,它是通过一个位于 grails-app/conf/UrlMappings.groovy 的URL映射类所控制.

UrlMappings类包含一个名为mappings单一属性,并被赋予一个代码块:

class UrlMappings {
    static mappings = {
    }	
}

6.4.1 映射到控制器和操作

为了创建简单的映射,只需简单的使用相对URL作为方法名,并指定控制器和操作的命名参数来映射:

"/product"(controller:"product", action:"list")

在这种情况下,我们建立URL/productProductControllerlist操作的映射。 你当然可以省略操作定义,来映射控制器默认的操作:

"/product"(controller:"product")

一个可选的语法是把在块中被赋值的控制器和操作传递给方法:

"/product" {
	controller = "product"
	action = "list"
}

你使用哪一个句法很大程度上依赖于个人偏好.

6.4.2 嵌入式变量

简单变量

早前的部分说明,怎样使用具体的"标记"来映射普通的URLs。在URL映射里讲过,标记是在每个斜线(/)字符之间的顺序字符。 一个具体的标记就像/product这样被良好定义。 然而,很多情况下,标记的值直到运行时才知道是什么。在这种情况下,你可以在URL中使用变量占位符,例如:

static mappings = {
  "/product/$id"(controller:"product")
}

在这种情况下,通过嵌入一个$id变量作为第2个标记,Grails将自动映射第2个标记到一个名为id的参数(通过params对象得到). 例如给定的URL/product/MacBook,下面的代码将渲染"MacBook"到响应中:

class ProductController {
     def index = { render params.id }
}

当然你可以构建更多复杂的映射示例。例如传统的blog URL格式将被映射成下面这样:

static mappings = {
   "/$blog/$year/$month/$day/$id"(controller:"blog", action:"show")
}

上面的映射允许你这样:

/graemerocher/2007/01/10/my_funky_blog_entry

在URL里单独的标记将再次被映射到带有year, month, day, id等等可用值的 params 对象中.

动态控制器(Controller)和操作(Action)名

变量同样可以被用于动态构造控制器和操作名。实际上,默认的Grails URL映射使用这样的技术:

static mappings = {
    "/$controller/$action?/$id?"()
}

这里,控制器(controller)名,操作(action)名和id名,隐式的从嵌入在URL中的controller, actionid中获得:

static mappings = {
    "/$controller" {
	   action = { params.goHere }
    }
}

可选的变量

默认映射另一个特性就是能够在一个变量的末尾附加一个? ,使它成为一个可选的标记。这个技术更进一步的示例能够运用于blog URL映射,使它具有更灵活性的连接 :

static mappings = {
   "/$blog/$year?/$month?/$day?/$id?"(controller:"blog", action:"show")
}

下列URLs的所有映射将与放置于params对象中的唯一关联的参数匹配:

/graemerocher/2007/01/10/my_funky_blog_entry
/graemerocher/2007/01/10
/graemerocher/2007/01
/graemerocher/2007
/graemerocher

任意变量

你同样可以传递来自于URL映射的任意参数给控制器,把他们设置在块内传递给这个映射:

"/holiday/win" {
     id = "Marrakech"
     year = 2007
}

在这个params对象得到的这个变量将被传递给这个控制器.

动态解析变量

硬编码任意变量是有用的,但是,有时你需要基于运行时因素来计算变量名。这个同样可能通过给变量名分配一个块:

"/holiday/win" {
     id = { params.id } 
     isEligible = { session.user != null } // must be logged in
}

上述情况,当URL实际被匹配,块中的代码将被解析,因此可以被用于结合所有种类的逻辑处理.

6.4.3 映射到视图

如果你想决定一个URL一个view,而无需涉及一个控制器或者操作,你也可以这样做。 例如,如果你想映射根URL / 到一个位于 grails-app/views/index.gsp 的GSP,你可以这样使用:

static mappings = {
      "/"(view:"/index")  // map the root URL
}

换句话说,假如你需要一个具体给定的控制器(Controller)中的一个视图,你可以这样使用:

static mappings = {
   "/help"(controller:"site",view:"help") // to a view for a controller
}

6.4.4 映射到响应代码

Grails同样允许你映射一个HTTP响应代码到控制器,操作或视图。所有你需要做的是使用一个方法名来匹配你所感兴趣的响应代码:

static mappings = {
   "500"(controller:"errors", action:"serverError")
   "404"(controller:"errors", action:"notFound")
   "403"(controller:"errors", action:"forbidden")
}

或者换句话说,假如你只不过想提供定制的错误页面:

static mappings = {
   "500"(view:"/errors/serverError")
   "404"(view:"/errors/notFound")
   "403"(view:"/errors/forbidden")
}

6.4.5 映射到HTTP方法

URL映射同样可以配置成基于HTTP 方法 (GET, POST, PUT or DELETE)的map。这个对于RESTful APIs和基于HTTP方法的约束映射是非常有用的.

作为一个示例,下面的映射为ProductControllerURL提供一个RESTful API URL映射:

static mappings = {
   "/product/$id"(controller:"product"){
       action = [GET:"show", PUT:"update", DELETE:"delete", POST:"save"]
   }	
}

6.4.6 映射通配符

Grails的URL映射机制同样支持通配符映射。例如,考虑下面的映射:

static mappings = {
	"/images/*.jpg"(controller:"image")
}

这个映射将匹配所有images路径下像/image/logo.jpg这样的jpg。当然你可以通过一个变量来达到同样的效果:

static mappings = {
	"/images/$name.jpg"(controller:"image")
}

然而,你可以使用双通配符来匹配多于一个层次之外的:

static mappings = {
	"/images/**.jpg"(controller:"image")
}

这样的话,这个映射将不但匹配/image/logo.jpg而且匹配/image/other/logo.jpg。更好的是你可以使用一个双通配符变量:

static mappings = {
	// will match /image/logo.jpg and /image/other/logo.jpg 
	"/images/$name**.jpg"(controller:"image")
}

这样的话,它将储存路径,从params 对象获得命名参数里的name通配符 :

def name = params.name
println name // prints "logo" or "other/logo"

如果你使用通配符URL mappings,那么你可以排除某些来自Grails的URL mapping进程 URIs. 实现这个你可以在UrlMappings.groovy类中设置excludes :

class UrlMappings = {
	static excludes = ["/images/**", "/css/**"]
	static mappings = {
		…
	}
}

这样,Grails不为匹配任何以 /images/css开头的URLs.

6.4.7 自动重写链接

URL映射另一个重要的特性是自动定制 link 标签的行为。以便改变这个映射而不需要改变所有的连接.

通过一个URL重写技术做到这点,从URL映射反转连接设计:

static mappings = {
   "/$blog/$year?/$month?/$day?/$id?"(controller:"blog", action:"show")
}

如果,你像下列一样使用连接标签:

<g:link controller="blog" action="show" params="[blog:'fred', year:2007]">My Blog</g:link>
<g:link controller="blog" action="show" params="[blog:'fred', year:2007, month:10]">My Blog - October 2007 Posts</g:link>

Grails将自动重写URL通过适当的格式:

<a href="/fred/2007">My Blog</a>
<a href="/fred/2007/10">My Blog - October 2007 Posts</a>

6.4.8 应用约束

URL映射同样支持Grails统一 验证规约 机制, 它允许你更进一步"约束"一个URL是怎么被匹配的。例如,如果我们回到早前的blog示例代码,这个映射当前看上去会像这样 :

static mappings = {
   "/$blog/$year?/$month?/$day?/$id?"(controller:"blog", action:"show")
}

允许URLs像这样:

/graemerocher/2007/01/10/my_funky_blog_entry

不过,它也允许这样:

/graemerocher/not_a_year/not_a_month/not_a_day/my_funky_blog_entry

当它强迫你在控制器代码中做一些聪明的语法分析时会有问题。幸运的是,URL映射能进一步的约束验证URL标记:

"/$blog/$year?/$month?/$day?/$id?" {
     controller = "blog"
     action = "show"
     constraints {
          year(matches:/d{4}/)
          month(matches:/d{2}/)
          day(matches:/d{2}/)
     }
}

在这种情况下,约束能确保 year, monthday参数匹配一个具体有效的模式,从而在稍后来减轻你的负担 .

6.5 Web流(Flow)

概述

Grails基于Spring Web Flow项目来支持创建Web流(Flow)。一个Web流(Flow)就是一个会话,它跨越多个请求并保持着流(Flow)作用域的状态。 一个Web流(Flow)也定义了开始和结束状态。 .

Web流(Flow)无需HTTP session,但作为替代,它将状态存储在序列化表单中,然后通过Grails来回传递的request参数中的执行流中的key进行还原。 这相比其他使用HttpSession来保存状态的应用来说更具有可扩展性,尤其是在内存和集群方面.

Web流(Flow)本质是高级的状态机,它管理着一个状态到下个状态"流"的执行。因为为你管理着状态,你就勿需担心用户在进入多步骤流(Flow)的操作(action) ,因为Web流(Flow)已经帮你管理了,因此Web流(Flow)在处理象网上购物、宾馆预定及任何多页面的工作流的应用具有出乎意料的简单.

创建流

创建一个流(Flow)只需简单的创建一个普通的Grails控制器(controller),然后添加一个以规约Flow结尾的操作。例如:

class BookController {
   def index = {
      redirect(action:"shoppingCart")
   }
   def shoppingCartFlow = {
        …
   }
}

注意,当重定向或引用流(Flow)时,可以把它当做一个操作(action)而省略掉流(Flow)前缀。换句话说, 上面流的操作(action)名为shoppingCart.

6.5.1 开始与结束状态

如上所述,一个流(Flow)定义了开始和结束状态。一个开始状态是当用户第一次开始一个会话(或流(Flow))。Grails的开始流(Flow)是第一个带有代码块的方法调用。例如:

class BookController {
   …
   def shoppingCartFlow = {
       showCart {
           on("checkout").to "enterPersonalDetails"           
           on("continueShopping").to "displayCatalogue"
       }
       …
       displayCatalogue {
            redirect(controller:"catalogue", action:"show")
       }
       displayInvoice()
   }
}

这里,showCart节点是这个流的开始状态。 因为这个showCart状态并没有定义一个操作(action)或重定向,只被视为是一个视图状态。 通过规约,指向grails-app/views/book/shoppingCart/showCart.gsp视图 .

注意,这不像正规的控制器(controller)操作(action),这个视图被存储于与其流名字匹配的grails-app/views/book/shoppingCart目录中 .

shoppingCart流(Flow)也可能拥有两个结束状态。第一个是displayCatalogue, 执行外部重定向到另一个控制器(controller)和操作(action),从而结束流(Flow)。第二个是displayInvoice是一个最终状态,因为它根本没有任何事件, 只是简单的渲染一个名为grails-app/views/book/shoppingCart/displayInvoice.gsp的视图,并在同一时间终止流(Flow).

一旦一个流(Flow)结束,它只能从开始状态重新开始,对于showCart不会来自任何其他状态.

6.5.2 操作(Action)状态和视图状态

视图状态

视图状态没有定义操作(action)redirect。下面是一个视图状态示例:

enterPersonalDetails {
   on("submit").to "enterShipping"
   on("return").to "showCart"
}

它默认查找一个名为grails-app/views/book/shoppingCart/enterPersonalDetails.gsp的视图。 注意,enterPersonalDetails定义了两个事件:submitreturn。视图负责触发(triggering)这些事件。假如你想让视图用于渲染,使用render方法来完成:

enterPersonalDetails {
   render(view:"enterDetailsView")
   on("submit").to "enterShipping"
   on("return").to "showCart"
}

现在,它将查找grails-app/views/book/shoppingCart/enterDetailsView.gsp。假如使用共享视图,视图参数以/ 开头:

enterPersonalDetails {
   render(view:"/shared/enterDetailsView")
   on("submit").to "enterShipping"
   on("return").to "showCart"
}

现在,它将查找 grails-app/views/shared/enterDetailsView.gsp

操作(Action)状态

操作(Action)状态只执行代码但不渲染任何视图。操作(Action)的结果被用于控制流(Flow)的切换。为了创建一个操作操作(Action)状态,你需要定义一个被用于执行的操作。 这通过调用action方法实现并传递它的一个代码块来执行:

listBooks {
   action { 
	  [ bookList:Book.list() ]
   }
   on("success").to "showCatalogue"
   on(Exception).to "handleError"
}

正如你看到的,一个操作看上去非常类似于一个控制器(controller)操作(action),实际上,假如你需要可以重用控制器(controller)操作(action)。 假如这个操作没有错误成功返回,success事件将被触发。 在这里,返回一个map,它被视为"model"看待,并自动放置于流(flow)作用域.

此外,在上面的示例中也使用了下面的异常处理程序来处理错误:

on(Exception).to "handleError"

这使当流(Flow)切换到状态出现异常的情况下调用handleError.

你可以编写与流(flow)请求上下文相互作用更复杂的操作(action):

processPurchaseOrder  {
     action {
         def a =  flow.address
         def p = flow.person
         def pd = flow.paymentDetails
         def cartItems = flow.cartItems
         flow.clear()

def o = new Order(person:p, shippingAddress:a, paymentDetails:pd) o.invoiceNumber = new Random().nextInt(9999999) cartItems.each { o.addToItems(it) } o.save() [order:o] } on("error").to "confirmPurchase" on(Exception).to "confirmPurchase" on("success").to "displayInvoice" }

这是一个更复杂的操作(action),用于收集所有来自流(flow)作用域信息,并创建一个Order对象。 然后,把Order作为模型返回。这里值得注意的重要事情是与请求上下文和 "流(flow)作用域"的相互作用.

切换操作

另一种形式的操作(action)被称之为切换操作(action)。一旦一个event被触发,切换操作优先于状态切换被直接执行。普通的切换操作如下 :

enterPersonalDetails {
   on("submit") {
       log.trace "Going to enter shipping"	
   }.to "enterShipping"
   on("return").to "showCart"
}

注意,我们是怎样传递一个代码块给submit事件,它只是简单的记录这个切换。切换状态对于数据绑定与验证是非常有用的,将在后面部分涵盖.

6.5.3 流(Flow)执行事件

为了执行流流从一个状态到下一个状态的 切换 ,你需要一些方法来触发一个 event ,指出流流下一步该做什么。事件的触发可以来自于任何视图状态和操作状态.

来自于一个视图状态的触发事件

正如之前所讨论的,在早前代码列表内流的开始状态可能处理两个事件。一个checkout和一个continueShopping事件:

def shoppingCartFlow = {
    showCart {
        on("checkout").to "enterPersonalDetails"           
        on("continueShopping").to "displayCatalogue"
    }
    …
}

因为showCart事件是一个视图状态,它会渲染 grails-app/book/shoppingCart/showCart.gsp视图. 在视图内部,你需要拥有一个用于触发流(Flow)执行的组件.在一个表单中,这可使用submitButton标签:

<g:form action="shoppingCart">
    <g:submitButton name="continueShopping" value="Continue Shopping"></g:submitButton>
    <g:submitButton name="checkout" value="Checkout"></g:submitButton>
</g:form>

这个表格必须提交返回shoppingCart流流。每个submitButton标签的name属性标示哪个事件将被触发。 假如,你没有表格,你同样可以用link标签来触发一个事件,如下:

<g:link action="shoppingCart" event="checkout" />

来自于一个操作(Action)的触发事件

为了触发来自于一个操作(action)的一个事件,你需要调用一个方法。例如,这里内置的error()success()方法。 下面的示例在切换操作中验证失败后触发error()事件:

enterPersonalDetails {
   on("submit") {
         def p = new Person(params)
         flow.person = p
         if(!p.validate())return error()
   }.to "enterShipping"
   on("return").to "showCart"
}

在这种情况下,因为错误,切换操作将使流回到enterPersonalDetails状态.

有了一种操作状态,你也能触发事件来重定向流:

shippingNeeded {
   action {
       if(params.shippingRequired) yes()
       else no()
   }
   on("yes").to "enterShipping"
   on("no").to "enterPayment"
}

6.5.4 流(Flow)的作用域

作用域基础

在以前的示例中,你可能会注意到我们在“流作用域(flow scope)”中已经使用了一个特殊的流(flow)来存储对象,在Grails中共有5种不同的作用域可供你使用 :

Grails的service类可以自动的定位web flow的作用域,详细请参考Services .

此外从一个action中返回的模型映射(model map)将会自动设置成flow范围,比如在一个转换(transition)的操作中,你可以象下面这样使用流(flow)作用域 :

enterPersonalDetails {
   on("submit") {
         [person:new Person(params)]
   }.to "enterShipping"
   on("return").to "showCart"
}

要知道每一个状态总是创建一个新的请求,因此保存在request作用域中的对象在其随后的视图状态中不再有效,要想在状态之间传递对象 ,需要使用除了request之外的其他作用域。此外还有注意,Web流(Flow)将 :

  1. 在状态转换的时候,会将对象从flash作用域移动到request作用域;
  2. 在渲染以前,将会合并flow和conversation作用域的对象到视图模型中(因此你不需要在视图中引用这些对象的时候,再包含一个作用域前缀了).

流(Flow)的作用域和序列化

当你将对象放到 flash, flowconversation 作用域中的时候,要确保对象已经实现了java.io.Serializable接口,否则将会报错。 这在domain类尤为显著,因为领域类通常在视图中渲染的时候被放到相应的作用域中,比如下面的领域类示例 :

class Book {
	String title
}

为了能够让Book类的实例可以放到流(flow)作用域中,你需要修改如下:

class Book implements Serializable {
	String title
}

这也会影响到领域类中的关联和闭包,看下面示例:

class Book implements Serializable {
	String title
	Author author
}

此处如果Author关联没有实现Serializable,你同样也会得到一个错误。 此外在GORM events中使用的闭包比如onLoad, onSave等也会受到影响, 下例的领域类如果放到flow作用域中,将会产生一个错误:

class Book implements Serializable {
	String title
	def onLoad = {
		println "I'm loading"
	}
}

这是因为onLoad事件中的代码块必能被序列化,要想避免这种错误,需要将所有的事件声明为transient :

class Book implements Serializable {
	String title
	transient onLoad = {
		println "I'm loading"
	}
}

6.5.5 数据绑定和验证

开始和结束状态 部分, 开始状态的第一个示例触发一个切换到 enterPersonalDetails 状态。这个状态渲染一个视图,并等待用户键入请求信息 :

enterPersonalDetails {
   on("submit").to "enterShipping"
   on("return").to "showCart"
}

一个视图包含一个带有两个提交按钮的表格,每个都触发提交事件或返回事件:

<g:form action="shoppingCart">
    <!-- Other fields -->
    <g:submitButton name="submit" value="Continue"></g:submitButton>
    <g:submitButton name="return" value="Back"></g:submitButton>
</g:form>

然而,怎么样捕捉被表格提交的信息?为了捕捉表格信息我们可以使用流切换操作:

enterPersonalDetails {
   on("submit") {
         flow.person = new Person(params)
         !flow.person.validate() ? error() : success()
   }.to "enterShipping"
   on("return").to "showCart"
}

注意,我们是怎样执行来自请求参数的绑定,把Person实体放置于流(flow)作用域中。同样有趣的是,我们执行 验证,并在验证失败是调用error()方法 .这个流(flow)的动机即停止切换并返回 enterPersonalDetails 视图,因此,有效的项通过user进入,否则,切换继续并转到enterShipping state.

就像正规操作(action),流(flow)操作(action)也支持 命令对象概念,通过定义闭包的第一个参数 :

enterPersonalDetails {
   on("submit") { PersonDetailsCommand cmd ->	     
          flow.personDetails = cmd
         !flow.personDetails.validate() ? error() : success()
   }.to "enterShipping"
   on("return").to "showCart"
}

6.5.6 子流程和会话

Grails的Web Flow集成同样支持子流(subflows)。一个子流在一个流中就像一个流。拿下面search流作为示例:

def searchFlow = {
            displaySearchForm {
                on("submit").to "executeSearch"
            }
            executeSearch {
                action {
                    [results:searchService.executeSearch(params.q)]
                }
                on("success").to "displayResults"
                on("error").to "displaySearchForm"
            }
            displayResults {
                on("searchDeeper").to "extendedSearch"
                on("searchAgain").to "displaySearchForm"
            }
            extendedSearch {
                subflow(extendedSearchFlow)   // <--- extended search subflow
                on("moreResults").to "displayMoreResults"
                on("noResults").to "displayNoMoreResults"
            }
            displayMoreResults()
            displayNoMoreResults()
}

它在extendedSearch状态中引用了一个子流。子流完全是另一个流 :

def extendedSearchFlow = {
       startExtendedSearch {
           on("findMore").to "searchMore"
           on("searchAgain").to "noResults"
       }
       searchMore {
           action {
              def results = searchService.deepSearch(ctx.conversation.query)
              if(!results)return error()
              conversation.extendedResults = results
           }
           on("success").to "moreResults"
           on("error").to "noResults"
       }
       moreResults()
       noResults()
}

注意,它是怎样把extendedResults放置于会话范围的。这个范围不同于流范围,因为它允许你横跨整个会话而不只是这个流。 同样注意结束状态(每个子流的 moreResultsnoResults在主流中触发事件 :

extendedSearch {
         subflow(extendedSearchFlow)   // <--- extended search subflow
         on("moreResults").to "displayMoreResults"
         on("noResults").to "displayNoMoreResults"
}

6.6 过滤器

尽管Grails支持良好的细粒度控制器(controller),但只对少数控制器(controller)的应用时非常有用,当管理大型应用时就会变得很困难。 另一方面,过滤器能横跨一群控制器(controller),一个URI空间或一个具体的操作(action)。 过滤器对插件更容易并能保证彻底的分离主要控制器(controller)逻辑,有利于所有像安全,日志等等这样的横切关注点 .

6.6.1 应用过滤器

为了创建一个过滤器,可在 grails-app/conf 下创建一个以规约 Filters 结尾的类。在这个类中,定义一个名为 filters 的代码块,它包含了过滤器的定义 :

class ExampleFilters {
   def filters = {
        // your filters here
   }
}

每个在filters块中定义的过滤器(Filters)拥有一个名字和一个 作用域。名字是方法的名字,作用域使用命名参数来定义。例如,假如你需要定义一个应用于所有控制器(controller)和操作(action)的过滤器(Filters)可以使用通配符 :

sampleFilter(controller:'*', action:'*') {
  // interceptor definitions
}

过滤器的作用域可以是下面之一:

过滤器的一些示例包括:

all(controller:'*', action:'*') {

}

justBook(controller:'book', action:'*') {

}

someURIs(uri:'/book/**') {

}

allURIs(uri:'/**') {

}

另外,这个次序决定了你所定义的过滤器的执行次序.

6.6.2 过滤器(Filters)类型

在过滤器的主体内,你可以定义下列过滤器(Filters)的拦截器类型之一:

例如,为实现普通身份验证,可以定义如下过滤器(Filters):

class SecurityFilters {
   def filters = {
       loginCheck(controller:'*', action:'*') {
           before = {
              if(!session.user && !actionName.equals('login')) {
                  redirect(action:'login')
                  return false
               }
           }

} } }

这里的loginCheck过滤器(Filters)使用一个before拦截器来执行代码块, 检查是否一个用户在session内,假如不是,重定向到login操作(action)。注意,如何返回false确保操作(action)本身不被执行 .

6.6.3 变量与作用域

过滤器支持所有在 控制器(controllers)标签库 中可用的属性,附加application context :

不过,过滤器只支持用于控制器(controller)和标签库方法的子集 。这些包括:

6.7 Ajax

Ajax代表异步Javascript与XML,它是转向富web应用程序的驱动力. 这些类型的应用程序,通常更适合于像RubyGroovy语言所写的敏捷,动态框架,Grails通过它的Ajax标签库提供支持构建Ajax应用程序. 它们完整的列表可以参看标签库参考.

6.7.1 用Prototype实现Ajax

Grails默认装载Prototype 库,但通过Plug-in 系统,可以提供对Dojo, Yahoo UIGoogle Web Toolkit 等其他框架的支持.

这部分涵盖Grails对Prototype的支持。你需要在页面的<head>标签内添加这样一行就可以开始了 :

<g:javascript library="prototype" />

这里使用javascript标签自动插入Prototype正确位置的引用。假如你同样需要Scriptaculous ,你可以如下这样做为替换 :

<g:javascript library="scriptaculous" />

6.7.1.1 远程链接

远程内容可以通过多种方式加载,最常使用的方法是通过 remoteLink 标签。 这个标签允许创建的HTML锚标记执行一个异步请求,并在一个元素中随意设置响应。用这个简单方式创建的远程链接就像这样 :

<g:remoteLink action="delete" id="1">Delete Book</g:remoteLink>

上面的连接发送一个异步请求给当前id为1的控制器的delete操作 .

6.7.1.2 内容更新

这真是太棒了,但通常你想提供一些事情发生的反馈信息给用户:

def delete = {
      def b = Book.get( params.id )
      b.delete()
      render "Book ${b.id} was deleted"
}

GSP代码:

<div id="message"></div>
<g:remoteLink action="delete" id="1" update="message">Delete Book</g:remoteLink>

上面的示例将调用这个操作并设置message div的响应内容为"Book 1 was deleted"。这通过标签上的update属性来完成,它同样可以获取一个map来指出在失败时什么被更新 :

<div id="message"></div>
<div id="error"></div>
<g:remoteLink action="delete" id="1"
              update="[success:'message',failure:'error']">Delete Book</g:remoteLink>

这里,error div在请求失败时被更新.

6.7.1.3 远程表单提交

,一个HTML form也可以异步被提交通过以下两种方式之一。第一个,使用 formRemote 标签,它和 remoteLink 标签有类似的属性 :

<g:formRemote url="[controller:'book',action:'delete']" update="[success:'message',failure:'error']">
       <input type="hidden" name="id" value="1" />
       <input type="submit" value="Delete Book!" />
</g:formRemote >

或者作为选择可以使用submitToRemote来创建一个提交按钮。它允许一些按钮远程提交而一些不依赖操作 :

<form action="delete">
       <input type="hidden" name="id" value="1" />
       <g:submitToRemote action="delete" update="[success:'message',failure:'error']" />
</form>

6.7.1.4 Ajax事件

某些事件的发生会调用特定的javascript。所有以"on"开头的事件,在适当的时候允许你反馈信息给用户,或采取其他行为:

<g:remoteLink action="show" 
              id="1" 
              update="success" 
              onLoading="showProgress()" 
              onComplete="hideProgress()">Show Book 1</g:remoteLink>

上述代码将执行"showProgress()"函数来显示一个进度条或者其他适当的展示,其他的事件还包括 :

假如你需要引用XmlHttpRequest对象,你可以使用隐式的event参数e获取它 :

<g:javascript>
   function fireMe(e) {
	   alert("XmlHttpRequest = " + e)
   }
}
</g:javascript>
<g:remoteLink action="example" 
              update="success" 
              onSuccess="fireMe(e)">Ajax Link</g:remoteLink>

6.7.2 用Dojo实现Ajax

Grails把 Dojo 作为一种外部插件来支持Grails的特性。在终端窗口,进入你项目的根目录键入下列命令来安装插件 :

grails install-plugin dojo

将下载Dojo最新的支持版本,并安装到你的Grails项目中。完成上面的步骤后,你可以在你页面的顶部添加下列引用:

<g:javascript library="dojo" />

现在,所有像remoteLink, formRemotesubmitToRemote标签都可以和Dojo进行远程处理工作 .

6.7.3 用GWT实现Ajax

Grails同样支持 Google Web Toolkit 特性,插件的全面 文档 可以在Grails wiki中找到 .

6.7.4 服务端的Ajax

虽然Ajax特性X为XML,但通常可以分解成许多不同方式执行Ajax:

Ajax部分中的更多的示例涵盖了内容为中心的 Ajax在什么地方更新页面,但同样你可能使用数据为中心的Ajax或脚本为中心的 Ajax。这份指南涵盖了不同风格的Ajax .

内容为中心的Ajax

作为概括,内容为中心的 Ajax涉及从服务器端发送一些HTML返回和通过使用render方法来渲染模板 :

def showBook = {
	def b = Book.get(params.id)

render(template:"bookTemplate", model:[book:b]) }

在客户端调用这个会涉及到remoteLink标签的使用 :

<g:remoteLink action="showBook" id="${book.id}" update="book${book.id}">Update Book</g:remoteLink>
<div id="book${book.id}">
   <!--existing book mark-up -->
</div>

数据为中心的Ajax与JSON

数据为中心的Ajax通常涉及到客户端响应的赋值和编程化更新。Grails中的JSON响应,通常使用Grails的JSON marshaling能力 :

import grails.converters.*

def showBook = { def b = Book.get(params.id)

render b as JSON }

然后,在客户端使用一个Ajax事件处理解析这个进入的JSON请求:

<g:javascript>
function updateBook(e) {
	var book = eval("("+e.responseText+")") // evaluate the JSON
	$("book"+book.id+"_title").innerHTML = book.title
}
<g:javascript>
<g:remoteLink action="test" update="foo" onSuccess="updateBook(e)">Update Book</g:remoteLink>
<g:set var="bookId">book${book.id}</g:set>
<div id="${bookId}">
	<div id="${bookId}_title">The Stand</div>
</div>

数据为中心的Ajax与XML

在服务器端使用XML同样普遍:

import grails.converters.*

def showBook = { def b = Book.get(params.id)

render b as XML }

不过,因为涉及到DOM,客户变得更复杂:

<g:javascript>
function updateBook(e) {
	var xml = e.responseXML
	var id = xml.getElementsByTagName("book").getAttribute("id")
	$("book"+id+"_title")=xml.getElementsByTagName("title")[0].textContent
}
<g:javascript>
<g:remoteLink action="test" update="foo" onSuccess="updateBook(e)">Update Book</g:remoteLink>
<g:set var="bookId">book${book.id}</g:set>
<div id="${bookId}">
	<div id="${bookId}_title">The Stand</div>
</div>

脚本为中心的Ajax与JavaScript

脚本为中心的 Ajax涉及实际返回的Javascript在客户端被赋值。这样的示例见下表:

def showBook = {
	def b = Book.get(params.id)

response.contentType = "text/javascript" String title = b.title.encodeAsJavascript() render "$('book${b.id}_title')='${title}'" }

要记住的重要事情是,设置contentTypetext/javascript。如果在客户端使用Prototype,由于设置了contentType, 返回的Javascript将自动被赋值.

很明显,在这种情况下,它是关键性的,你有一个一致的client-sideAPI, 因此,你不想客户端的改变破坏服务器端。这就是Rails有些像RJS的理由之一。 虽然,Grails当前没有像RJS的一个特性,但动态Dynamic JavaScript Plug-in插件提供了类似的能力.

6.8 内容协商

Grails已经内置支持内容协商通过使用任意HTTP Accept 报头 ,一种明确格式请求参数或URI映射的扩展.

配置Mime类型

在你开始处理内容协商之前,你必须告诉Grails希望支持什么样的内容类型。 默认情况下,grails-app/conf/Config.groovy内使用 grails.mime.types设置来配置若干不同的内容类型 :

grails.mime.types = [ xml: ['text/xml', 'application/xml'],
                      text: 'text-plain',
                      js: 'text/javascript',
                      rss: 'application/rss+xml',
                      atom: 'application/atom+xml',
                      css: 'text/css',
                      cvs: 'text/csv',
                      all: '*/*',
                      json: 'text/json',
                      html: ['text/html','application/xhtml+xml']
                    ]

上面的小块配置,允许Grails检查把包含 'text/xml' 或 'application/xml' 媒体类型的一个请求的格式只当做 'xml'看待,你可以添加你自己的类型通过简单的添加条目到 map中.

内容协商使用Accept报头

每个进入的HTTP请求都有个指定的Accept报头,它定义了什么样的媒体类型(或 mime 类型)客户端能"接受"。在老式浏览器中通常是 :

*/*

这意味着任何事物.不过在新生浏览器中,所有东西一起像这样发送更有用(一个FirefoxAccept报头示例) :

text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5

Grails解析这个进入的格式,并添加一个propertyrequest对象,用于描叙首选的请求格式。对于上述示例下列的断言会通过 :

assert 'html' == request.format

为什么?这个text/html媒体类型拥有最高"质量"等级0.9,因此,具有最高优先权。如前所述,假如你有一老式浏览器结果会稍微不同 :

assert 'all' == request.format

在这种情况下,'all'可能的格式会被客户端接受。为了处理来自控制器(Controllers)不同类型的请求,你可以使用withFormat方法,它的行为被当作switch表达式 :

import grails.converters.*

class BookController { def books def list = { this.books = Book.list() withFormat { html bookList:books js { render "alert('hello')" } xml { render books as XML } } } }

当Grails只执行 html()调用并且首选的格式是html时会发生什么。它只是让 Grails寻找每个名为grails-app/views/books/list.html.gspgrails-app/views/books/list.gsp的视图。 如果格式是xml,那么,闭包会被调用,XML响应会被渲染 .

我们怎样处理'all'格式?只需在withFormat代码块中简单指定content-types,以便,无论你想要的哪个都会被首先执行。 因此,在上面示例中的"all" 将触发html处理 .

当使用withFormat时确保它在控制器(controller)操作(action)中最后一个被调用,因为withFormat方法的返回值用来决定操作(action)下一步做什么.

内容协商与格式化请求参数

如果请求头的内容跟你的不一致,通过指定一个format的请求参数覆盖这个格式 :

/book/list?format=xml

你同样可以在URL Mappings定义中定义这个参数 :

"/book/list"(controller:"book", action:"list") {
	format = "xml"
}

内容协商与URI扩展

Grails同样可以通过URI扩展支持内容协商。例如,给定下列URI:

/book/list.xml

Grails将剔除扩展并映射到/book/list作为替代,同时,基于这个扩展把内容格式化为xml。 这个行为是默认允许的,那么,假如你希望关闭它, 你必须把grails-app/conf/Config.groovy下的grails.mime.file.extensions属性设置为false :

grails.mime.file.extensions = false

测试内容协商

为了在一个综合测试中测试内容协商(参见 测试部分)你可以操作每个进入的请求包头 :

void testJavascriptOutput() {
	def controller = new TestController()
	controller.request.addHeader "Accept", "text/javascript, text/html, application/xml, text/xml, */*"

controller.testAction() assertEquals "alert('hello')", controller.response.contentAsString }

或者你可以设置格式化参数来实现类似的效果:

void testJavascriptOutput() {
	def controller = new TestController()
	controller.params.format = 'js'

controller.testAction() assertEquals "alert('hello')", controller.response.contentAsString }